"""
    @file   PyAstTransformer.py
    @brief  converting AST into a more simple hierarchy required for
            simple analyzing of Python code.

    It does build up an own hierarchy for
    <ul>
        <li>each class (name, line number, lines of code, doc comment, list of methods)
        <li>each method or function (name, line number, parameter names, lines of code, doc comment, parent)
    </ul>

    What you can do with it is
    <ul>
        <li>checking for number of lines per function or method.
        <li>checking for number of parameter per function or method.
        <li>checking for documentation of a class, function or method.
        <li>checking for number of functions and methods per class.
        <li>checking for number of functions and methods per file.
        <li>checking for number of classes per file.
    </ul>
"""
import ast
import os

class Class(object):
    """ represents a Python class """
    def __init__(self, name, lineNr):
        """ initializes members; name and lineNr are given;
            the other members will be set later on

            @param name    is the name of the class.
            @param lineNr  is the line where the class definition starts.
        """
        self.name     = name
        self.lineNr   = lineNr
        self.lines    = 0
        self.comment  = None
        self.methods  = []

    def __repr__(self):
        """ delivery basic information as string:
            name, lineNr, LOC, doc, number of methods
        """
        text  = "Class \"%(name)s\""            % self.__dict__
        text += " at line %(lineNr)d"           % self.__dict__
        text += " with %(lines)d lines of code" % self.__dict__
        if len(self.methods) > 0:
            text += ", %d methods"              % len(self.methods)
        if self.comment:
            text += "\n ... %(comment)s"        % self.__dict__
        return text

class Method(object):
    """ represents a Python method """
    def __init__(self, name, lineNr, arguments = []):
        """ initializes members; name, lineNr and argument names are given;
            the other members will be set later on

            @param name      is the name of the method.
            @param lineNr    is the line where the method definition starts.
            @param arguments name of the parameters of the method
        """
        self.name      = name
        self.lineNr    = lineNr
        self.arguments = arguments
        self.lines     = 0
        self.comment   = None
        self.parent    = None
        self.cstms     = 0

    def __repr__(self):
        """ basic information about given method """
        text  = "Method \"%(name)s\""           % self.__dict__
        text += " at line %(lineNr)d"           % self.__dict__
        text += " with %(lines)d lines of code" % self.__dict__
        text += ", arguments: %(arguments)s"    % self.__dict__
        if self.parent:
            text += ", in class \"%(name)s\""   % self.parent.__dict__
        if self.comment:
            text += "\n ... %(comment)s"        % self.__dict__
        return text

class Function(object):
    """ represents a Python function """
    def __init__(self, name, lineNr, arguments = []):
        """ initializes members; name, lineNr and argument names are given;
            the other members will be set later on

            @param name      is the name of the function.
            @param lineNr    is the line where the function definition starts.
            @param arguments name of the parameters of the function
        """
        self.name      = name
        self.lineNr    = lineNr
        self.arguments = arguments
        self.lines     = 0
        self.comment   = None
        self.parent    = None
        self.cstms     = 0

    def __repr__(self):
        """ basic information about given function """
        text  = "Function \"%(name)s\""         % self.__dict__
        text += " at line %(lineNr)d"           % self.__dict__
        text += " with %(lines)d lines of code" % self.__dict__
        text += ", arguments: %(arguments)s"    % self.__dict__
        if self.parent:
            text += ", in class \"%(name)s\""   % self.parent.__dict__
        if self.comment:
            text += "\n ... %(comment)s"        % self.__dict__
        return text

class Comment(object):
    """ represents a Python comment for a class, function or method """
    def __init__(self, text, lineNr):
        """ initializes members; text and lineNr are given;

            @param text      documentation string
            @param lineNr    is the line where the documentation ends.
        """
        self.text   = text
        self.lineNr = lineNr
        self.lines  = self.text.count("\n") + 1

    def isValid(self):
        """ a comment requires at least 4 different words """
        return len(set(word for word in self.text.split())) >= 4

    def __repr__(self):
        """ representation of a documentation string """
        return "\"\"\"" + self.text.replace("\n", "\\n") + "\"\"\""

class ControlStatement(object):
    """ represents a Python control statement like if, elif, else, while and for """
    IF     = "if"
    FOR    = "for"
    WHILE  = "while"
    RETURN = "return"

    def __init__(self, name, lineNr, column):
        self.name   = name
        self.lineNr = lineNr
        self.column = column
        self.lines  = 0

    def __repr__(self):
        text  = "Statement \"%(name)s\""        % self.__dict__
        text += " at line %(lineNr)d"           % self.__dict__
        text += " with %(lines)d lines of code" % self.__dict__
        return text

class PyAstTransformer(ast.NodeVisitor):
    """ This class is transforming all information of interests into another more
        readable and usable data hierarchy. Goal is to to be able to check
        different kind of metrics.
    """
    def __init__(self):
        """  initialization of members only """
        self.objects         = []
        self.stack           = []
        self.lineNr          = 0
        self.pathAndFileName = ""

    def analyze(self, pathAndFileName):
        """ walking the AST hierarchy transforming AST nodes into classes which
            provide a better and easier way of analyzing Python code.

            @param pathAndFileName path and name of file for analyze
            @return True, when successful
        """
        if not os.path.isfile(pathAndFileName):
            return False

        self.pathAndFileName = pathAndFileName
        self.visit(ast.parse(open(pathAndFileName).read()))
        return True

    def visit_ClassDef(self, node):
        """ called when the AST library has detected a class definition in Python """
        newClass = Class(node.name, node.lineno)

        self.objects.append(newClass)
        self.stack.append(newClass)
        self.generic_visit(node)
        self.stack.pop()
        newClass.lines = self.lineNr - newClass.lineNr

    def visit_FunctionDef(self, node):
        """ called when the AST library has detected a function or method definition in Python """
        arguments = node.args.args

        if not len(arguments):
            object = Function(node.name, node.lineno)
        else:
            # it's depending on the Python version: "id" or "arg"
            if "id" in dir(arguments[0]):
                arguments = [arg.id for arg in arguments]
            else:
                arguments = [arg.arg for arg in arguments]

            # a method always has "self" as first parameter
            if arguments[0] == "self":
                object = Method(node.name, node.lineno, arguments)
            else:
                object = Function(node.name, node.lineno, arguments)

            # a method usually refers to a class so we add it there
            if len(self.stack) > 0:
                object.parent = self.stack[-1]
                if isinstance(self.stack[-1], Class):
                    self.stack[-1].methods.append(object)

        self.objects.append(object)
        self.stack.append(object)
        # traversing the content of the function or method.
        self.generic_visit(node)
        self.stack.pop()
        # number of line of current function or method.
        object.lines = self.lineNr - object.lineNr

    def visit_Str(self, node):
        """ called when the AST library has detected a string. The intention
            here is to scan for documentation of classes, functions and methods.
        """
        if len(self.stack) > 0:
            newComment = Comment(node.s, node.lineno)
            if newComment.isValid():
                if self.stack[-1].lineNr == self.lineNr-newComment.lines:
                    self.stack[-1].comment = newComment
            else:
                del newComment

        ast.NodeVisitor.generic_visit(self, node)

    def visit_If(self, node):
        object = ControlStatement(ControlStatement.IF, node.lineno, node.col_offset)

        if len(self.stack) > 0:
            object.parent = self.stack[-1]
            if isinstance(self.stack[-1], Function) or isinstance(self.stack[-1], Method):
                self.stack[-1].cstms += 1

        self.objects.append(object)
        ast.NodeVisitor.generic_visit(self, node)
        object.lines = self.lineNr - object.lineNr

    def visit_For(self, node):
        object = ControlStatement(ControlStatement.FOR, node.lineno, node.col_offset)

        if len(self.stack) > 0:
            object.parent = self.stack[-1]
            if isinstance(self.stack[-1], Function) or isinstance(self.stack[-1], Method):
                self.stack[-1].cstms += 1

        self.objects.append(object)
        ast.NodeVisitor.generic_visit(self, node)
        object.lines = self.lineNr - object.lineNr

    def visit_Return(self, node):
        object = ControlStatement(ControlStatement.RETURN, node.lineno, node.col_offset)
        self.objects.append(object)
        ast.NodeVisitor.generic_visit(self, node)

    def generic_visit(self, node):
        """ the method is called on each node. It's a kind of dispatcher.
            Here it's used to check for the current line. This allows
            to calculate the number of lines per block (class, function,
            method, if, while, ...)
        """
        if "lineno" in dir(node):
            self.lineNr = node.lineno

        ast.NodeVisitor.generic_visit(self, node)

    def dump(self):
        """ dumping all objects relying on the implementations of __repr__ """
        for object in self.objects:
            print(object)

def main():
    """ main function to run the transformer against itself """
    transformer = PyAstTransformer()
    transformer.analyze("PyAstTransformer.py")
    transformer.dump()

if __name__ == "__main__":
    main()
